#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

# ====SLOTS==== 
class Student(object):
    pass

s = Student()

# we can set a new method for an instance
def new_method(self, attr):
    self.attr = attr

from types import MethodType

s.new_method = MethodType(new_method, s) # 给实例绑定一个方法
s.new_method(42)
print(s.attr)
# but new_method() will not work for another instance of the class
 
def method2(self, attr2):
    self.attr2 = attr2

Student.method2 = method2 # set the method to the class

s.method2('so long')
print(s.attr2)
# from above, we can see dynamic lang like python can easily set new attr to class

class Teacher(object):
    __slots__ = ('name', 'age') # __slots__ can restrict attr names tuple of a class

t = Teacher()
try:
    t.gender = 'male' # so now we cannot set a attr named 'gender'
except:
    print('we cannot set attr names not in the __slots__ tuple!')

class Grade1(Teacher):
    pass

g1 = Grade1() # inherited class do not inherit __slots__
g1.gender = 'female'
print(g1.gender)

class Grade2(Teacher):
    __slots__ = ('gender') # but if the daughter class also has a __slots__

g2 = Grade2()
g2.gender = 'secret'
g2.name = 'X' # the restriction tuple will include the __slots__ of mother class
try:
    t.hobby = '?' # so now we cannot set a attr named 'gender'
except:
    print('we cannot set attr names not in the __slots__ tuple!')
 
# ====@PROPERTY==== 
# to avoid unproperly manipulation of attrs of object, we often def get_attr and set_attr methods
# but there is easier way:
class StudentP(object):

    @property
    def score(self): # a getter method
        return self._score

    @score.setter # if we do not set this @xx.setter, xx become a read-only attr.
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

sp = StudentP()
sp.score = 60 # equal to set_score
print(sp.score) # equal to get_score
sp.score = 999 # raise ValueError


# ====MULTIPLE INHERITANCE==== 
class Animal(object):
    pass

class Mammal(Animal):
    pass

class Flyable(object):
    def fly(self):
        print('flying...')

class Bat(Animal, Flyable):
    pass # has attrs of both Animal and Flyable

# multiple inheritance is also called MixIn, which is used in many build-in class.


# ====CUSTOM CLASS==== 
class StudentC:
    def __init__(self, name):
        self.__name = name
    
    def __str__(self): # __str__() define what to print when the instance is directly called by print()
        return 'Student object (name: %s)' % self.__name
    
    __repr__ = __str__ # __repr__ define the output in python terminal when the instance is invoked

chris = StudentC('Chris')
print(chris)

# __iter__ make a object iterable, continuously call __next__ of the returned object
class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a，b

    def __iter__(self):
        return self # 实例本身就是迭代对象，故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值

    def __getitem__(self, n): 
        if isinstance(n, int): # __getitem__ make object can fetch item by index, like object[n]
            a, b = 1, 1
            for i in range(n):
                a, b = b, a+b
            return a
        if isinstance(n, slice): # even slice object[start:stop]
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L # but here we cannot handle [::step] and [-2:] of slice feature

    def __getattr__(self, attr): # if a undefined attr is called, object will try calling this method
        if attr=='score':
            return 99
        raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

    def __call__(self): # execute when directly invoke instance
        print('My name is Fib.') 


for n in Fib():
    print(n) # print Fib range

print(Fib[1:6]) # 1 2 3 5 8
print(Fib.score) # 99
Fib() # 'My name is Fib.'

# ====ENUMERATION==== 
# good way to create constant value set
from enum import Enum, unique

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
# Month is now daughter class of class Enum

print(Month.Jan)
for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value) # .value is automatically ordered from 1

@unique # the decorator will ensure there is no duplicate value
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

day1 = Weekday.Mon
print(day1) # Weekday.Mon
print(Weekday.Tue) # Weekday.Tue
print(Weekday['Tue']) # Weekday.Tue
print(Weekday.Tue.value) # 2
print(day1 == Weekday.Mon) # True
print(day1 == Weekday.Tue) # False
print(Weekday(1)) # Weekday.Mon
print(day1 == Weekday(1)) # True
Weekday(7) # ValueError: 7 is not a valid Weekday

for name, member in Weekday.__members__.items():
    print(name, '=>', member)
'''
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
'''

# ====METACLASS====

class Hello:
    pass

h = Hello()
print(type(Hello)) # <class 'type'>
print(type(h)) # <class 'hello.Hello'>

# type() can also create new class
def fn(self, name='world'): # 先定义函数
    print('Hello, %s.' % name)

Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
# type('Name of class', (inherited class tuple), dict(method=func))

# class can be seen as instance of metaclass
# metaclass是类的模板，所以必须从`type`类型派生：
class ListMetaclass(type): # 默认习惯，metaclass的类名总是以Metaclass结尾，以便清楚地表示这是一个metaclass
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

class MyList(list, metaclass=ListMetaclass):
    pass

'''
下略
“元类就是深度的魔法，99%的用户应该根本不必为此操心。
如果你想搞清楚究竟是否需要用到元类，那么你就不需要它。
那些实际用到元类的人都非常清楚地知道他们需要做什么，而且根本不需要解释为什么要用元类。”
  ——Tim Peters
'''